#!/user/bin/env python
# -*- coding: utf-8 -*-
"""
@Time   : 2023-02-23 15:25
@Author : WJQ
@File   : page.py
"""

# here put the import lib

from typing import List
from appium.webdriver import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from appium.webdriver.webelement import WebElement
from typing import Union
from lxml import etree
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

import conf
from models.Node import Component
from utils.common import CrawlerLogger, parse_element
from selenium.common.exceptions import TimeoutException, NoSuchElementException, WebDriverException, JavascriptException


class PageHandler(object):
    def __init__(self, driver):
        self.driver = driver

    @staticmethod
    def safe_find_elements ( driver: webdriver, by: str, locator: str, timeout=20 ) :
        driver.implicitly_wait(timeout)
        try :
            elements = WebDriverWait(driver, timeout).until(EC.presence_of_all_elements_located((by, locator)))
        except (TimeoutException, NoSuchElementException, WebDriverException) as e:
            CrawlerLogger.error(f"[-] Error finding elements at {locator} by {e}")
            elements = []
        finally :
            driver.implicitly_wait(5)
        return elements

    @staticmethod
    def safe_find_element ( driver: webdriver, by: str, locator: str, timeout=1 ) :
        driver.implicitly_wait(timeout)
        try :
            element = WebDriverWait(driver, timeout).until(EC.presence_of_element_located((by, locator)))
        except (TimeoutException, NoSuchElementException, WebDriverException):
            element = None
        finally :
            driver.implicitly_wait(5)
        return element

    @staticmethod
    def safe_find_element_visible ( driver: webdriver, by: str, locator: str, timeout=1 ) :
        driver.implicitly_wait(timeout)
        try :
            elements = WebDriverWait(driver, timeout).until(EC.presence_of_all_elements_located((by, locator)))
            for element in elements:
                if 0 < element.rect['x'] <= conf.CLIENT_SIDE_CONF.PHONE_WIDTH and \
                        0 < element.rect['y'] <= conf.CLIENT_SIDE_CONF.PHONE_HEIGHT :
                    return element
            element = None
        except (TimeoutException, NoSuchElementException, WebDriverException):
            element = None
        finally :
            driver.implicitly_wait(5)
        return element

    @staticmethod
    def safe_click_element ( driver: webdriver, by: str, locator: str, timeout=1 ) :
        driver.implicitly_wait(timeout)
        try :
            WebDriverWait(driver, timeout).until(EC.presence_of_element_located((by, locator))).click()
        except (TimeoutException, NoSuchElementException, WebDriverException) as e :
            CrawlerLogger.error(f"[-] Click error at {locator} by {e}")
        except AttributeError as e :
            CrawlerLogger.error(f"[-] AttributeError at {locator} by {e}")
        finally :
            driver.implicitly_wait(5)

    @staticmethod
    def safe_click_elements ( driver: webdriver, by: str, locator: str, timeout=1 ) :
        driver.implicitly_wait(timeout)
        elements = PageHandler.safe_find_elements(driver, by, locator, timeout)
        for element in elements:
            try:
                element.click()
            except Exception as e:
                CrawlerLogger.error(f"[-] Click error at {locator} by {e}")

        driver.implicitly_wait(5)


    ''' 
        解析页面可点击的组件功能代码
    '''

    def get_right_locations ( self, elements: List, scale_ratio_width: float, scale_ratio_height: float,
                              top_bar_height: float ) -> List :
        '''
        Get the actual position of elements in a webview.
        :param elements: List of web elements
        :param scale_ratio_width: Scale ratio for width
        :param scale_ratio_height: Scale ratio for height
        :param top_bar_height: Height of the top bar
        :return: List of element details
        '''
        script = """
            var elements = arguments[0];
            var scaleRatioWidth = arguments[1];
            var scaleRatioHeight = arguments[2];
            var topBarHeight = arguments[3];
            var results = [];

            for (var i = 0; i < elements.length; i++) {
                var rect = elements[i].getBoundingClientRect();
                var tagName = elements[i].tagName.toLowerCase();
                var src = elements[i].getAttribute('src');
                var text = (tagName === 'wx-image' && src && src.startsWith('https')) ? '图片' : elements[i].innerText;
                var className = elements[i].className;

                results.push({
                    'rect': {
                        'x': rect.left * scaleRatioWidth,
                        'y': (rect.top * scaleRatioHeight) + topBarHeight,
                        'width': rect.width * scaleRatioWidth,
                        'height': rect.height * scaleRatioHeight
                    },
                    'tagName': tagName,
                    'text': text,
                    'className': className,
                });
            }
            return results;
        """
        try :
            # Get the position and size of elements
            locations = self.driver.execute_script(script, elements, scale_ratio_width, scale_ratio_height,
                                                   top_bar_height)
            return locations
        except JavascriptException as e :
            print(f"Error getting right locations: {e}")
            return []

    def merged_intractable_elements( self, intractable_elements_list ) -> List[Component]:
        intractable_elements_list.sort(key=lambda comp : comp.x)

        # Merge adjacent elements
        merged_x_elements = []
        for component in intractable_elements_list :
            if merged_x_elements and merged_x_elements[-1].x == component.x and \
                    abs(merged_x_elements[-1].y - component.y) < 150 :
                merged_x_elements[-1].merge(component)
            else :
                merged_x_elements.append(component)

        merged_x_elements.sort(key=lambda comp : comp.y)
        # Merge adjacent elements
        merged_y_elements = []
        for component in merged_x_elements :
            if merged_y_elements and merged_y_elements[-1].y == component.y and \
                    abs(merged_y_elements[-1].x - component.x) < 150 :
                merged_y_elements[-1].merge(component)
            else :
                merged_y_elements.append(component)

        return merged_y_elements

    def safe_get_all_display_elements(self, by: str, locator: str) -> List[Component]:
        '''
        通过tag Name 获得可点击的组件
        :param by: By 类型
        :param locator: locator
        :return:
        '''
        # 获取设备的屏幕尺寸
        screen_size = conf.CLIENT_SIDE_CONF.PHONE_WIDTH, conf.CLIENT_SIDE_CONF.PHONE_HEIGHT
        # 获取缩放后的 WebView 窗口大小
        scale_window_size = self.driver.execute_script('return {height: window.innerHeight, width: window.innerWidth};')
        # 计算缩放比例
        scale_ratio_width = screen_size[0] / scale_window_size['width']
        # scale_ratio_height = screen_size[1] / scale_window_size['height']

        try:
            elements = PageHandler.safe_find_elements(self.driver, by, locator)
            locations = self.get_right_locations(elements[:200], scale_ratio_width, scale_ratio_width, conf.CLIENT_SIDE_CONF.TOP_BAR_HEIGHT)
            sorted_locations = sorted(locations, key=lambda loc : loc['rect']['y'])
            intractable_elements = []

            for location in sorted_locations :
                if 0 < location['rect']['x'] <= conf.CLIENT_SIDE_CONF.PHONE_WIDTH and \
                    0 < location['rect']['y'] <= conf.CLIENT_SIDE_CONF.PHONE_HEIGHT and \
                    location['text'] != "" :
                        component = Component(location)
                        if component not in intractable_elements :
                            intractable_elements.append(component)

                # Break the loop if the y value exceeds PHONE_HEIGHT + 200
                if location['rect']['y'] > (conf.CLIENT_SIDE_CONF.PHONE_HEIGHT + 200) :
                    break
            return self.merged_intractable_elements(intractable_elements)

        except Exception as e:
            CrawlerLogger.debug(f"[-] Get element error at {self.driver.path}: {repr(e)}")
            return []

    def get_webview ( self, root ) :
        # 使用XPath查找所有tag为"android.webkit.WebView"的节点
        webviews = root.xpath(".//android.webkit.WebView")

        # 生成一个以title为key，以对应webview为value的字典
        title_webview_dict = {webview.get("text") : webview for webview in webviews}

        # 寻找第一个text中不包含":"的webview
        for title, webview in title_webview_dict.items() :
            if ":" not in title :
                return webview

        # 如果所有的webview都不满足条件，返回第一个webview，如果列表为空返回None
        return next(iter(title_webview_dict.values()), None)

    def safe_get_all_native_display_elements(self) -> List[Component]:
        page_source = self.driver.page_source
        root = etree.fromstring(page_source.encode('utf-8'))

        # 使用XPath查找第一个tag为"android.webkit.WebView"的节点
        # webview = root.find(".//android.webkit.WebView")
        webview = self.get_webview(root)

        # 使用XPath查找第一个tag为"android.webkit.WebView"的节点
        # webview = root.find(".//android.webkit.WebView")

        if webview is not None :
            intractable_elements = []
            clickable_elements = webview.findall(".//android.view.View")
            non_edit_text_elements = [elem for elem in clickable_elements if 'android.widget.EditText' not in elem.tag]
            image_elements = webview.findall(".//android.widget.Image")
            non_edit_text_elements.extend(image_elements)
            text_elements = webview.findall(".//android.widget.TextView")
            non_edit_text_elements.extend(text_elements)

            for element in non_edit_text_elements:
                # 使用正则表达式解析bounds属性
                component = parse_element(element)
                if component and component not in intractable_elements:
                    # 将Component对象添加到结果列表中
                    intractable_elements.append(component)

            return self.merged_intractable_elements(intractable_elements)
        else :
            return []

    @staticmethod
    def get_all_click_elements( driver: webdriver ) -> List[Component] :
        instance = PageHandler(driver)
        try:
            if conf.WEBVIEW_MODE:
                complete_xpath = " | ".join(conf.CLIENT_SIDE_CONF.TAG_XPATH)
                return instance.safe_get_all_display_elements(AppiumBy.XPATH, complete_xpath)
            else:
                return instance.safe_get_all_native_display_elements()
        except Exception as e:
            CrawlerLogger.debug(f"[-] Get native element error: {repr(e)}")
            return []

    '''
        判断页面元素的功能代码
    '''

    @staticmethod
    def find_parent_element(driver: webdriver, element: WebElement) -> Union[WebElement, None]:
        try:
            return driver.execute_script("return arguments[0].parentNode;", element)
        except JavascriptException as e:
            CrawlerLogger.debug(f"Error finding parent element: {e}")
            return None

    @staticmethod
    def find_preceding_siblings(driver: webdriver, element: WebElement) -> int:
        script = """
            var children = Array.from(arguments[0].parentNode.children);
            var siblings = children.slice(0, children.indexOf(arguments[0]));
            return siblings.filter(e => e.tagName.toLowerCase() === arguments[1].toLowerCase());
        """
        try:
            preceding_siblings = driver.execute_script(script, element)
            return len(preceding_siblings)
        except JavascriptException as e:
            CrawlerLogger.debug(f"Error finding preceding siblings: {e}")
            return 0

    @staticmethod
    def find_xpath_by_element(driver: webdriver, element: WebElement) -> str:
        # Get the element's complete XPath
        xpath_parts = []
        while element.tag_name != 'html':
            tag = element.tag_name
            index = PageHandler.find_preceding_siblings(driver, element) + 1
            xpath_parts.append(f"{tag}[{index}]")
            element = PageHandler.find_parent_element(driver, element)

        xpath_parts.append('html[1]')
        xpath_parts.reverse()
        xpath = '/' + '/'.join(xpath_parts)

        # Return the XPath
        return xpath

    @staticmethod
    def find_elements_with_text(driver: webdriver):
        # 获取所有含有text的元素
        elements = PageHandler.safe_find_elements(driver, AppiumBy.XPATH, "//wx-view[text()[normalize-space()]]")

        result = []
        for element in elements:
            # 检查元素的兄弟节点是否有text
            siblings = PageHandler.find_preceding_siblings(driver, element)
            if siblings:
                result.append(element)
                continue

            # 如果没有，向上查找父节点的兄弟节点
            parent = PageHandler.find_parent_element(driver, element)
            while parent:
                siblings = PageHandler.find_preceding_siblings(driver, parent)
                if siblings:
                    result.append(element)
                    break
                parent = PageHandler.find_parent_element(driver, parent)

        return result

    @staticmethod
    def get_label(driver: webdriver, element: WebElement):
        try:
            label = driver.execute_script(
                "return Array.from(arguments[0].parentNode.children).find(e => e !== arguments[0] && e.nodeType === Node.ELEMENT_NODE);",
                element)
            return label.text if label.text else element.text
        except Exception as e:
            return None

    @staticmethod
    def is_webview_scrollable(driver: webdriver) -> bool:
        script = """
        return document.body.scrollHeight > window.innerHeight || 
               document.body.scrollWidth > window.innerWidth;
        """
        try:
            return driver.execute_script(script)
        except JavascriptException as e:
            print(f"Error checking if webview is scrollable: {e}")
            return False

    @staticmethod
    def fill_element(driver: webdriver, element: WebElement, text: str):
        try:
            driver.execute_script("arguments[0].querySelector('div:nth-child(1) > div:nth-child(1)').textContent = arguments[1];", element, text)
        except JavascriptException as e:
            print(f"Error filling element: {e}")